package bsh;

import java.io.*;
import java.util.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**

@author Pat Niemeyer (pat@pat.net)
 */
public class ClassGeneratorImpl extends ClassGenerator {

    public Class generateClass(
            String name, Modifiers modifiers,
            Class[] interfaces, Class superClass, BSHBlock block,
            boolean isInterface, CallStack callstack, Interpreter interpreter)
            throws EvalError {
        // Delegate to the static method
        return generateClassImpl(name, modifiers, interfaces, superClass,
                block, isInterface, callstack, interpreter);
    }

    public Object invokeSuperclassMethod(
            BshClassManager bcm, Object instance, String methodName, Object[] args)
            throws UtilEvalError, ReflectError, InvocationTargetException {
        // Delegate to the static method
        return invokeSuperclassMethodImpl(bcm, instance, methodName, args);
    }

    /**
    Change the parent of the class instance namespace.
    This is currently used for inner class support.
    Note: This method will likely be removed in the future.
     */
    // This could be static
    public void setInstanceNameSpaceParent(
            Object instance, String className, NameSpace parent) {
        This ithis =
                ClassGeneratorUtil.getClassInstanceThis(instance, className);
        ithis.getNameSpace().setParent(parent);
    }

    /**
    Parse the BSHBlock for for the class definition and generate the class
    using ClassGenerator.
     */
    public static Class generateClassImpl(
            String name, Modifiers modifiers,
            Class[] interfaces, Class superClass, BSHBlock block,
            boolean isInterface, CallStack callstack, Interpreter interpreter)
            throws EvalError {
        // Scripting classes currently requires accessibility
        // This can be eliminated with a bit more work.
        try {
            Capabilities.setAccessibility(true);
        } catch (Capabilities.Unavailable e) {
            throw new EvalError(
                    "Defining classes currently requires reflective Accessibility.",
                    block, callstack);
        }

        NameSpace enclosingNameSpace = callstack.top();
        String packageName = enclosingNameSpace.getPackage();
        String className = enclosingNameSpace.isClass ? (enclosingNameSpace.getName() + "$" + name) : name;
        String fqClassName =
                packageName == null ? className : packageName + "." + className;

        BshClassManager bcm = interpreter.getClassManager();
        // Race condition here...
        bcm.definingClass(fqClassName);

        // Create the class static namespace
        NameSpace classStaticNameSpace =
                new NameSpace(enclosingNameSpace, className);
        classStaticNameSpace.isClass = true;

        callstack.push(classStaticNameSpace);

        // Evaluate any inner class class definitions in the block
        // effectively recursively call this method for contained classes first
        block.evalBlock(
                callstack, interpreter, true/*override*/,
                ClassNodeFilter.CLASSCLASSES);

        // Generate the type for our class
        Variable[] variables =
                getDeclaredVariables(block, callstack, interpreter, packageName);
        DelayedEvalBshMethod[] methods =
                getDeclaredMethods(block, callstack, interpreter, packageName);

        ClassGeneratorUtil classGenerator = new ClassGeneratorUtil(
                modifiers, className, packageName, superClass, interfaces,
                variables, methods, classStaticNameSpace, isInterface);
        byte[] code = classGenerator.generateClass();

        // if debug, write out the class file to debugClasses directory
        String dir = System.getProperty("debugClasses");
        if (dir != null) {
            try {
                FileOutputStream out =
                        new FileOutputStream(dir + "/" + className + ".class");
                out.write(code);
                out.close();
            } catch (IOException e) {
            }
        }

        // Define the new class in the classloader
        Class genClass = bcm.defineClass(fqClassName, code);

        // import the unq name into parent
        enclosingNameSpace.importClass(fqClassName.replace('$', '.'));

        try {
            classStaticNameSpace.setLocalVariable(
                    ClassGeneratorUtil.BSHINIT, block, false/*strictJava*/);
        } catch (UtilEvalError e) {
            throw new InterpreterError("unable to init static: " + e);
        }

        // Give the static space its class static import
        // important to do this after all classes are defined
        classStaticNameSpace.setClassStatic(genClass);

        // evaluate the static portion of the block in the static space
        block.evalBlock(
                callstack, interpreter, true/*override*/,
                ClassNodeFilter.CLASSSTATIC);

        callstack.pop();

        if (!genClass.isInterface()) {
            // Set the static bsh This callback
            String bshStaticFieldName = ClassGeneratorUtil.BSHSTATIC + className;
            try {
                LHS lhs = Reflect.getLHSStaticField(genClass, bshStaticFieldName);
                lhs.assign(
                        classStaticNameSpace.getThis(interpreter), false/*strict*/);
            } catch (Exception e) {
                throw new InterpreterError("Error in class gen setup: " + e);
            }
        }

        bcm.doneDefiningClass(fqClassName);
        return genClass;
    }

    static Variable[] getDeclaredVariables(
            BSHBlock body, CallStack callstack, Interpreter interpreter,
            String defaultPackage) {
        List vars = new ArrayList();
        for (int child = 0; child < body.jjtGetNumChildren(); child++) {
            SimpleNode node = (SimpleNode) body.jjtGetChild(child);
            if (node instanceof BSHTypedVariableDeclaration) {
                BSHTypedVariableDeclaration tvd =
                        (BSHTypedVariableDeclaration) node;
                Modifiers modifiers = tvd.modifiers;

                String type = tvd.getTypeDescriptor(
                        callstack, interpreter, defaultPackage);

                BSHVariableDeclarator[] vardec = tvd.getDeclarators();
                for (int i = 0; i < vardec.length; i++) {
                    String name = vardec[i].name;
                    try {
                        Variable var = new Variable(
                                name, type, null/*value*/, modifiers);
                        vars.add(var);
                    } catch (UtilEvalError e) {
                        // value error shouldn't happen
                    }
                }
            }
        }

        return (Variable[]) vars.toArray(new Variable[0]);
    }

    static DelayedEvalBshMethod[] getDeclaredMethods(
            BSHBlock body, CallStack callstack, Interpreter interpreter,
            String defaultPackage)
            throws EvalError {
        List methods = new ArrayList();
        for (int child = 0; child < body.jjtGetNumChildren(); child++) {
            SimpleNode node = (SimpleNode) body.jjtGetChild(child);
            if (node instanceof BSHMethodDeclaration) {
                BSHMethodDeclaration md = (BSHMethodDeclaration) node;
                md.insureNodesParsed();
                Modifiers modifiers = md.modifiers;
                String name = md.name;
                String returnType = md.getReturnTypeDescriptor(
                        callstack, interpreter, defaultPackage);
                BSHReturnType returnTypeNode = md.getReturnTypeNode();
                BSHFormalParameters paramTypesNode = md.paramsNode;
                String[] paramTypes = paramTypesNode.getTypeDescriptors(
                        callstack, interpreter, defaultPackage);

                DelayedEvalBshMethod bm = new DelayedEvalBshMethod(
                        name,
                        returnType, returnTypeNode,
                        md.paramsNode.getParamNames(),
                        paramTypes, paramTypesNode,
                        md.blockNode, null/*declaringNameSpace*/,
                        modifiers, callstack, interpreter);

                methods.add(bm);
            }
        }

        return (DelayedEvalBshMethod[]) methods.toArray(
                new DelayedEvalBshMethod[0]);
    }

    /**
    A node filter that filters nodes for either a class body static
    initializer or instance initializer.  In the static case only static
    members are passed, etc.
     */
    static class ClassNodeFilter implements BSHBlock.NodeFilter {

        public static final int STATIC = 0,  INSTANCE = 1,  CLASSES = 2;
        public static ClassNodeFilter CLASSSTATIC =
                new ClassNodeFilter(STATIC);
        public static ClassNodeFilter CLASSINSTANCE =
                new ClassNodeFilter(INSTANCE);
        public static ClassNodeFilter CLASSCLASSES =
                new ClassNodeFilter(CLASSES);
        int context;

        private ClassNodeFilter(int context) {
            this.context = context;
        }

        public boolean isVisible(SimpleNode node) {
            if (context == CLASSES) {
                return node instanceof BSHClassDeclaration;
            }

            // Only show class decs in CLASSES
            if (node instanceof BSHClassDeclaration) {
                return false;
            }

            if (context == STATIC) {
                return isStatic(node);
            }

            if (context == INSTANCE) {
                return !isStatic(node);
            }

            // ALL
            return true;
        }

        boolean isStatic(SimpleNode node) {
            if (node instanceof BSHTypedVariableDeclaration) {
                return ((BSHTypedVariableDeclaration) node).modifiers != null && ((BSHTypedVariableDeclaration) node).modifiers.hasModifier("static");
            }

            if (node instanceof BSHMethodDeclaration) {
                return ((BSHMethodDeclaration) node).modifiers != null && ((BSHMethodDeclaration) node).modifiers.hasModifier("static");
            }

            // need to add static block here
            if (node instanceof BSHBlock) {
                return false;
            }

            return false;
        }
    }

    public static Object invokeSuperclassMethodImpl(
            BshClassManager bcm, Object instance, String methodName, Object[] args)
            throws UtilEvalError, ReflectError, InvocationTargetException {
        String superName = ClassGeneratorUtil.BSHSUPER + methodName;

        // look for the specially named super delegate method
        Class clas = instance.getClass();
        Method superMethod = Reflect.resolveJavaMethod(
                bcm, clas, superName, Types.getTypes(args), false/*onlyStatic*/);
        if (superMethod != null) {
            return Reflect.invokeMethod(
                    superMethod, instance, args);
        }

        // No super method, try to invoke regular method
        // could be a superfluous "super." which is legal.
        Class superClass = clas.getSuperclass();
        superMethod = Reflect.resolveExpectedJavaMethod(
                bcm, superClass, instance, methodName, args,
                false/*onlyStatic*/);
        return Reflect.invokeMethod(superMethod, instance, args);
    }
}
